using System;
using System.Collections.Generic;
using System.Text;
using System.Configuration;
using System.Configuration.Internal;
using System.Xml;
using System.Xml.Serialization;
using System.Reflection;
using System.CodeDom;
using System.CodeDom.Compiler;
using Microsoft.CSharp;
using Microsoft.VisualBasic;

namespace DirectoryWatcher
{
    /// <summary>
    /// A snippet of runtime-compiled code that we are to run in response to a filesystem event.
    /// </summary>
    public class ProgramCode : ConfigurationElement
    {
        /// <summary>
        /// The text of the actual code that we are to compile.
        /// </summary>
        private string text = null;
        /// <summary>
        /// Line number in the configuration file for this element (used for possible exception 
        /// messages).
        /// </summary>
        private int lineNumber = 0;
        /// <summary>
        /// Path to the configuration file in which this element resides (used for possible
        /// exception messages).
        /// </summary>
        private string fileName = "";
        /// <summary>
        /// Assembly that results when we compile the code.
        /// </summary>
        private Assembly assembly = null;

        /// <summary>
        /// The language for this code snippet.
        /// </summary>
        [ConfigurationProperty("language", IsRequired = true)]
        public ProgramLanguage Language
        {
            get
            {
                return (ProgramLanguage)base["language"];
            }
        }

        /// <summary>
        /// Text representing the actual code.
        /// </summary>
        public string Text
        {
            get
            {
                return text;
            }
        }

        /// <summary>
        /// Collection of referenced assemblies for this snippet of code.
        /// </summary>
        [ConfigurationProperty("referencedAssemblies")]
        public ReferencedAssemblyCollection ReferencedAssemblies
        {
            get
            {
                return (ReferencedAssemblyCollection)base["referencedAssemblies"];
            }
        }

        /// <summary>
        /// Assembly representing the compiled results of the code.
        /// </summary>
        public Assembly Assembly
        {
            get
            {
                // If the code has not already been compiled, do so now
                if (assembly == null)
                {
                    CodeDomProvider codeProvider = null;

                    // Get the proper code provider based on the code's language
                    if (Language == ProgramLanguage.CSharp)
                        codeProvider = new CSharpCodeProvider();

                    else if (Language == ProgramLanguage.VisualBasic)
                        codeProvider = new VBCodeProvider();

                    CompilerParameters compilerParameters = new CompilerParameters();

                    // Set the compiler options so that we don't generate an assembly on disk and
                    // we create a library assembly that does not contain debug information
                    compilerParameters.GenerateExecutable = false;
                    compilerParameters.GenerateInMemory = true;
                    compilerParameters.IncludeDebugInformation = false;
                    compilerParameters.CompilerOptions = "/target:library /optimize";
                    compilerParameters.ReferencedAssemblies.Add("System.dll");
                    compilerParameters.ReferencedAssemblies.Add(AppDomain.CurrentDomain.BaseDirectory + "\\DirectoryWatcher.exe");

                    // Add any assembly references (besides System.dll and DirectoryWatcher.exe, 
                    // which everyone gets) specified for this code
                    foreach (ReferencedAssembly referencedAssembly in ReferencedAssemblies)
                        compilerParameters.ReferencedAssemblies.Add(referencedAssembly.Name);

                    // Generate the assembly
                    CompilerResults results = codeProvider.CompileAssemblyFromSource(compilerParameters, text);

                    // Check the return code and throw an exception if the compilation failed
                    if (results.NativeCompilerReturnValue != 0)
                        throw new CompilationException(results.Errors);

                    assembly = results.CompiledAssembly;
                }

                return assembly;
            }
        }

        /// <summary>
        /// Handler for the case where we encounter an unrecognized element while attempting to 
        /// deserialize the class from XML; deals with "custom" properties, specifically the Text 
        /// property, whose value is set in an element node instead of an attribute.
        /// </summary>
        /// <param name="elementName">
        /// Name of the unrecognized element.
        /// </param>
        /// <param name="reader">
        /// Reader object that is involved in the deserialization.
        /// </param>
        /// <returns>
        /// True if we actually recognize the element, false otherwise.
        /// </returns>
        protected override bool OnDeserializeUnrecognizedElement(string elementName, XmlReader reader)
        {
            if (elementName == "text")
            {
                text = reader.ReadString();
                reader.Read();

                return true;
            }

            return base.OnDeserializeUnrecognizedElement(elementName, reader);
        }

        /// <summary>
        /// Instantiates the object using data stored in XML; records the filename and line number
        /// (for use later in possible exceptions) and then calls the base method.
        /// </summary>
        /// <param name="reader">
        /// Reader object that is involved in the deserialization.
        /// </param>
        /// <param name="serializeCollectionKey">
        /// True to serialize only the collection key properties, false otherwise.
        /// </param>
        protected override void DeserializeElement(XmlReader reader, bool serializeCollectionKey)
        {
            lineNumber = ((IConfigErrorInfo)reader).LineNumber;
            fileName = ((IConfigErrorInfo)reader).Filename;

            base.DeserializeElement(reader, serializeCollectionKey);
        }

        /// <summary>
        /// Called after the deserialization process is complete; validates the object's data.
        /// </summary>
        protected override void PostDeserialize()
        {
            if (text == null)
                throw new ConfigurationErrorsException("\"text\" is a required element.", fileName, lineNumber);

            base.PostDeserialize();
        }
    }
}
